本次的程式碼與目錄結構可以參考 FastAPI Tutorial : Day07 branch
我們目前已經知道如何使用 FastAPI 來建立一個簡單的 API,並且使用 Pydantic 來定義 Schema,並且使用 Response model 來定義回傳的資料格式。接下來我們將會再談談 Python Typing 與 Schema 常見錯誤。
在看這個錯誤之前,我們先來調整一下目前的專案結構 
先將原本 main.py 中的 fake_db 移到 database/fake_db.py 中
mkdir database
touch database/fake_db.py
先將 fake_db['users'] 改為 list 
並為 fake_db['users'] 中的每個 dict加上 id 、 password 和 avatar 欄位
fake_db = {
    "users": 
        [
            {
                "id": 1,
                "password": "John",
                "avatar": "https://i.pravatar.cc/300",
                "name": "John",
                "age": 35,
                "email": "john@fakemail.com",
                "birthday": "2000-01-01",
            },
            {
                "id": 2,
                "password": "Jane",
                "avatar": None,
                "name": "Jane",
                "age": 25,
                "email": "jane@fakemail.com",
                "birthday": "2010-12-04",
            }
        ]
    ,
    "items": 
    [
        {
            "id": 1,
            "name": "iPhone 12",
            "price": 1000,
            "brand": "Apple"
        },
        {
            "id": 2,
            "name": "Galaxy S21",
            "price": 800,
            "brand": "Samsung"
        }
    ]    
}
在 main.py 中引入 database.fake_db 並調整 get_db 的回傳值
from database.fake_db import get_db
# ...
fake_db = get_db()
# ...
再次調整 user 的 schemaschemas/user.py
class UserBase(BaseModel):
    id: int
class UserCreate(UserBase):
    password: str # 新增 password 欄位
    name: str
    avatar: str # 新增 avatar 欄位
    age: int
    email: str
    birthday: date
class UserRead(UserBase):
    name: str
    email: str
    avatar: str
最後因應 fake_db 調整 API 中 CRUD 的寫法:main.py
@app.get("/users/{user_id}" , response_model=UserSchema.UserRead )
def get_user_by_id(user_id: int, qry: str = None):
    for user in fake_db["users"]:
        if user["id"] == user_id:
            return user
        
    return {"error": "User not found"}
@app.post("/users" , response_model=UserSchema.UserCreateResponse )
def create_users(user: UserSchema.UserCreate ):
    fake_db["users"].append(user)
    return user
@app.delete("/users/{user_id}" )
def delete_users(user_id: int):
    
    for user in fake_db["users"]:
        if user["id"] == user_id:
            fake_db["users"].remove(user)
            return user
        
    return {"error": "User not found"}
在調整完專案結構後 ( 剛剛為 UserCreate 加上 password 和 avatar 欄位 ) 
我們來看看第一個 schema 常見錯誤 
我們先來測試 create user 的 API

會發現 Response model 如果同樣也使用 UserCreate 的話 
其中的 password 會以明文的方式回傳 
這是非常危險的 !!!
所以正確來說,我們應該還再需要寫一個 UserCreateResponse 
作為 create user 的 response_model 
schemas/user.py
class UserCreateResponse(UserBase):
    name: str
    email: str
main.py 的 create_user API
@app.post("/users" , response_model=UserSchema.UserCreateResponse ) # 改為 UserCreateResponse
def create_users(user: UserSchema.UserCreate ):
    fake_db = get_db()
    fake_db["users"][user.id] = user
    return user
更新過後的 API Response :
第一個常見的錯誤就是,沒有為 涵蓋敏感資訊 的 API 
設定專屬的 response schema ! 
接下來我們來看看第二個 schema 常見錯誤 
先來打一下 GET /users/2 的 API 看看

發現回傳 500 Internal Server Error 
看一下 server 的 log

上面寫著 avatar should be valid string 
但 input 的是 None 
我們檢查一下 fake_db 中 users[2] 的資料
2: {
        "id": 2,
        "password": "Jane",
        "avatar": None,
        "name": "Jane",
        "age": 25,
        "email": "jane@fakemail.com",
        "birthday": "2010-12-04",
    }
因為 avatar 欄位是 None 
導致 pydantic 驗證失敗 
所以對於 可能為 null(None) 的欄位 
我們應該要使用 Optional 或 Union 來定義 
接下來我們來看看第三個 schema 常見錯誤 
先來打一下 GET /users/99 的 API 看看

會發現 500 Internal Server Error 
查看 server log 後
上面寫著 Pydantic error 
是因為 user not found 的情況回傳的 {"error": "User not found"} 
並不是 UserRead 的 schema 所造成的 
所以我們可以透過 raise HTTPException 來處理 main.py
from fastapi import FastAPI, HTTPException # 引入 HTTPException
# ...
@app.get("/users/{user_id}" , response_model=UserSchema.UserRead )
def get_user_by_id(user_id: int, qry: str = None):
    for user in fake_db["users"]:
        if user["id"] == user_id:
            return user
        
    raise HTTPException(status_code=404, detail="User not found")
再次打一下 GET /users/99 的 API 看看 
這樣就不會回傳 500 Internal Server Error 了 
而是回傳 404 status code 與 {"detail": "User not found"} 
我們可以從 typing 中引入 Optional 和 Union Optional 用來定義 可能為 null(None) 的欄位 
schemas/user.py
from typing import Optional
class UserRead(UserBase):
    class UserRead(UserBase):
    name: str
    email: str
    avatar: Optional[str] = None
並且使用 Optional 的話,必須要有賦予值 
這邊讓 avatar 的預設值為 None 
除了 Optional 我們也可以透過 Union 的方式來定義可能為 null 的欄位 schemas/user.py
from typing import Union
class UserRead(UserBase):
    class UserRead(UserBase):
    name: str
    email: str
    avatar: Union[str,None] = None
接著去打一個 user 沒有 avatar 的 API  

我們假設 user 的 avatar 可以不填寫 ( 可能有用戶不想要設定頭像 ) 
所以在 create user 的 API 中,我們也要將 avatar 設定為 Optional 
schemas/user.py
class UserCreate(UserBase):
    password:str
    name: str
    avatar: Optional[str] = None
    age: int
    email: str
    birthday: date
再以不帶入 avatar 的情況打 create user 的 API 
也不會報錯了 
在說明 List 之前,我們先加上新的 API 
( 取得 user list )main.py
@app.get("/users")
def get_users(qry: str = None):
    return fake_db['users']
那要如何為 get_users 的 API 加上 response_model 呢 ? 
這時候就可以透過 List 來定義 
main.py
@app.get("/users", response_model=List[UserSchema.UserRead])
def get_users(qry: str = None):
    return fake_db['users']
這樣在 Swagger docs 中就可以看到 get_users 以 List schema 回傳的結果了 
而在 Python 3.9 之後,也可以使用內建的 list 來定義 main.py
@app.get("/users", response_model=list[UserSchema.UserRead])
def get_users(qry: str = None):
    return fake_db['users']
Optional 或 Union 來定義 schemaraise HTTPException 來處理List 來定義回傳的資料為 list